package jgame.impl;

import jgame.*;
import java.util.Vector;
import java.util.Hashtable;
import java.util.Enumeration;
import java.util.Random;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

/** Contains the platform-independent game logic. */
public class EngineLogic {

	JGImage imageutil;

	Random random;

	/**
	 * make_bitmask indicates what to do with transparent images. true = make
	 * bitmask false=make translucent
	 */
	boolean make_bitmask;

	/**
	 * prescale indicates if: (1) images should be prescaled to screen
	 * resolution or kept at their original size; (2) tiles should be drawn
	 * prescaled. It affects: scaled_tile*: scaled tile size, or original size
	 * width/height: rounded to whole tile sizes or actual window size
	 * *ofs_scaled: either scaled, or same as *ofs. Indirectly affected:
	 * *_scale_fac canvas_*ofs
	 */
	boolean prescale;

	public EngineLogic(JGImage imageutil, boolean make_bitmask, boolean prescale) {
		this.imageutil = imageutil;
		this.make_bitmask = make_bitmask;
		this.prescale = prescale;
		random = new Random();
		bg_images.addElement(null);
	}

	/** indicates if setCanvasSettings was called. */
	public boolean view_initialised = false;

	public JGColor fg_color = JGColor.white;
	public JGColor bg_color = JGColor.black;
	public JGFont msg_font = null;

	public int outline_thickness = 0;
	public JGColor outline_colour = JGColor.black;

	public double fps = 35;
	public double maxframeskip = 4.0; /* max # of frames to skip */

	public double gamespeed = 1.00000000001;

	/* game state. These vectors are always reused and not reconstructed. */

	/** Engine game state */
	public Vector gamestate = new Vector(10, 20);
	/** New engine game state to be assigned at the next frame */
	public Vector gamestate_nextframe = new Vector(10, 20);
	/**
	 * New game states which the game has to transition to, and for which
	 * start[state] have to be called.
	 */
	public Vector gamestate_new = new Vector(10, 20);
	/**
	 * indicates when engine is inside a parallel object update (moveObjects,
	 * check*Collision)
	 */

	boolean in_parallel_upd = false;

	private Vector timers = new Vector(20, 40);

	/**
	 * signals that JGame globals are set, and exit code should null globals in
	 * JGObject
	 */
	public boolean is_inited = false;
	/** signals that thread should die and canvas should paint exit message */
	public boolean is_exited = false;
	public String exit_message = "JGEngine exited successfully";

	Hashtable animations = new Hashtable();

	/* images */

	/**
	 * Strings -&gt; JGImages, original size, nonexistence means there is no
	 * image
	 */
	public Hashtable images_orig = new Hashtable();
	/** JGPoint sizes of original images */
	public Hashtable image_orig_size = new Hashtable();
	/**
	 * Strings -&gt; JGImages, screen size, nonexistence indicates image is not
	 * cached and needs to be generated from images_orig
	 */
	public Hashtable images = new Hashtable();

	/** indicates that image is defined even if it has no Image */
	public Hashtable images_exists = new Hashtable();
	public Hashtable images_transp = new Hashtable();
	/**
	 * Hashtable: name to filename. Indicates that image with given name is
	 * loaded from given filename
	 */
	public Hashtable images_loaded = new Hashtable();
	/* Integers -> Objects, existence indicates transparency */
	public Hashtable images_tile = new Hashtable(); /* Integers -> Strings */
	public Hashtable images_bbox = new Hashtable(); /* Strings -> Rectangles */
	public Hashtable images_tilecid = new Hashtable(); /* Integers -> Integers */

	public Hashtable imagemaps = new Hashtable(); /* Strings->ImageMaps */

	public int alpha_thresh = 128;
	public JGColor render_bg_color = null; // null means use bg_color

	/* objects */

	/**
	 * Note: objects lock is used to synchronise object updating between repaint
	 * thread and game thread. The synchronize functions are found in
	 * Engine.doFrameAll and Canvas.paint
	 */
	public SortedArray objects = new SortedArray(80); /* String->JGObject */
	SortedArray obj_to_remove = new SortedArray(40); /* String */
	Vector obj_spec_to_remove = new Vector(20, 40); /* (String,Int) */
	SortedArray obj_to_add = new SortedArray(40); /* JGObject */

	/* shared playfield dimensions */

	/**
	 * Total number of tiles on the playfield. Initially is the same as the nr
	 * of tiles on the visible window (viewnrtilesx/y), but can be changed to
	 * define a playfield larger than the visible window.
	 */
	public int nrtilesx, nrtilesy;
	/** Size of one tile */
	public int tilex, tiley;
	/** Number of tiles in view (visible window). */
	public int viewnrtilesx, viewnrtilesy;

	/* scaling preferences */

	public double min_aspect = 3.0 / 4.0, max_aspect = 4.0 / 3.0;
	public int crop_top = 0, crop_left = 0, crop_bottom = 0, crop_right = 0;
	public boolean smooth_magnify = true;

	/* playfield dimensions from canvas */

	/**
	 * Actual scaled canvas size; that is, the size of the playfield view, which
	 * may be smaller than the desired size of the game window to accommodate
	 * for integer-sized scaled tiles.
	 */
	public int width, height;
	/** Derived info, used for modulo calculation */
	public int pfwidth_half, pfheight_half;
	/** Derived info (playfield size in logical pixels) */
	public int pfwidth, pfheight;

	/**
	 * offset of playfield wrt canvas (may be negative if we crop the
	 * playfield).
	 */
	public int canvas_xofs = 0, canvas_yofs = 0;

	/** Size of one tile, scaled */
	public int scaledtilex, scaledtiley;

	/**
	 * Pending pixel offset of visible view on playfield, to be handled at the
	 * next frame draw.
	 */
	public static int pendingxofs = 0, pendingyofs = 0;
	/** Pixel offset of visible view on playfield. */
	public int xofs = 0, yofs = 0;
	/**
	 * Tile offset of top left tile on background image; is derived from x/yofs
	 */
	public int tilexofs = -1, tileyofs = -1;
	/** Derived offset information, useful for scaling. */
	public int xofs_scaled = 0, yofs_scaled = 0;
	/** Derived offset information, useful for modulo. */
	public int xofs_mid, yofs_mid;

	/**
	 * min_scale_fac is min (scalex,scaley). These are is 1.0 until width,
	 * height are defined
	 */
	public double x_scale_fac = 1.0, y_scale_fac = 1.0, min_scale_fac = 1.0;

	/* playfield dimensions from engine */

	/**
	 * Desired width/height of game window; 0 is not initialised yet. Note that
	 * the width/height of the canvas may be a little smaller to accommodate
	 * integer-sized scaled tiles.
	 */
	public int winwidth = 0, winheight = 0;

	/* background */

	public int[][] tilemap = null;
	public int[][] tilecidmap = null;
	// public boolean [] [] tilechangedmap=null;

	/** Wrap-around playfield */
	public boolean pf_wrapx = false, pf_wrapy = false;
	public int pf_wrapshiftx = 0, pf_wrapshifty = 0;

	public class BGImage {
		/**
		 * Image name (not tile name) of image to use behind transparent tiles.
		 */
		public String imgname;
		public boolean wrapx, wrapy;
		public JGPoint tiles;
		public double xofs = 0, yofs = 0;

		public BGImage(String imgname, boolean wrapx, boolean wrapy) {
			this.imgname = imgname;
			this.wrapx = wrapx;
			this.wrapy = wrapy;
			tiles = new JGPoint((JGPoint) image_orig_size.get(imgname));
			tiles.x /= tilex;
			tiles.y /= tiley;
		}

		@Override
		public boolean equals(Object obj) {
			if (obj != null) {
				if (obj instanceof BGImage) {
					System.out.println("bgi " + ((BGImage) obj).imgname);
					return this.imgname.equals(((BGImage) obj).imgname);
				}
				if (obj instanceof String)
					System.out.println("string " + this.imgname + obj);
				return this.imgname.equals(obj);
			}
			return false;
		}
	}

	/**
	 * BGImages: images to use behind transparent tiles. Element 0 is always
	 * defined. Null indicates empty image.
	 */
	public Vector bg_images = new Vector(8, 20);

	// public String bg_image=null;
	// public JGPoint bg_image_tiles=null;

	String out_of_bounds_tile = "";
	int out_of_bounds_cid = 0;
	int preserve_cids = 0;

	public int offscreen_margin_x = 16, offscreen_margin_y = 16;

	/**
	 * the defined state of the physical cells of background, i.e. (0,0) is the
	 * top left of background.
	 */
	public boolean[][] bg_defined = null;

	public int viewWidth() {
		return viewnrtilesx * tilex;
	}

	public int viewHeight() {
		return viewnrtilesy * tiley;
	}

	public int tileWidth() {
		return tilex;
	}

	public int tileHeight() {
		return tiley;
	}

	/**
	 * Replacement for stringTokenizer. str will be split into a Vector of
	 * tokens. Empty tokens are skipped. splitchar is a single character
	 * indicating a token boundary (multiple characters are not used here). The
	 * split characters are not included.
	 */
	public static Vector tokenizeString(String str, char splitchar) {
		Vector tok = new Vector(20, 50);
		int curidx = 0, nextidx;
		while ((nextidx = str.indexOf(splitchar, curidx)) >= 0) {
			if (nextidx > curidx)
				tok.addElement(str.substring(curidx, nextidx));
			curidx = nextidx + 1;
		}
		if (curidx < str.length())
			tok.addElement(str.substring(curidx));
		return tok;
	}

	/**
	 * Readline as in BufferedReader. Skips empty lines!
	 */
	public static String readline(InputStreamReader in) {
		int ch;
		StringBuffer line = null;
		try {
			while (true) {
				ch = in.read();
				if (ch == -1) {
					if (line == null)
						return null;
					return line.toString();
				}
				if (ch == 10 || ch == 13) {
					// eol if we read other characters, ignore if not
					if (line != null)
						return line.toString();
				} else {
					if (line == null)
						line = new StringBuffer();
					line.append((char) ch);
				}
			}
		} catch (IOException e) {
			if (line == null)
				return null;
			return line.toString();
		}
	}

	/**
	 * Generate absolute path from relative path by prepending the package name
	 * of this class (and converting the "." to "/". A relative path is a path
	 * without "/" at the beginning. Absolute paths are not changed.
	 */
//	public static String getAbsolutePath(Object pkg_obj, String filename) {
//		return filename;
//		if (filename.indexOf("\\") == 0) {
//			String file = filename.replace("\\", "/");
//			return file.substring(1, filename.length());
//		}
//		if (filename.indexOf("/") == 0) {
//			// path starts with "/": do not change
//			return filename;
//		} else {
//			// path does not start with "/": prepend package name
//			// with "." in package replaced by fileseparator
//			// this doesn't work as getPackage may return null for applets
//			// String pkgname = getClass().getPackage().getName();
//			String pkgname = pkg_obj.getClass().getName();
//			String pkgname_path = "";
//			Vector tokens = tokenizeString(pkgname, '.');
//			// StringTokenizer toker = new StringTokenizer(pkgname,".");
//			for (Enumeration e = tokens.elements(); e.hasMoreElements();) {
//				String tok = (String) e.nextElement();
//				if (e.hasMoreElements()) {
//					pkgname_path += tok + "/";
//				}
//			}
//			return "/" + pkgname_path + filename;
//		}
//	}

	/* images */

	/** protected */
	public boolean existsImage(String imgname) {
		return images_exists.containsKey(imgname);
	}

	/**
	 * Protected.
	 * 
	 * @param tileid
	 *            tile id number as Integer object (note: 0 is undefined)
	 * @return the image object, null means not defined
	 */
	public Object getTileImage(Integer tileid) {
		// we assume that images_tile will not contain id for tileid==0
		// if (tileid==0) return null;
		// Integer tileid_obj = new Integer(tileid);
		String imgid = (String) images_tile.get(tileid);
		if (imgid == null)
			return null;
		// XXX we assume images have already been converted from images_orig
		return images.get(imgid);
	}

	/** Gets (non-scaled) image's physical size directly. */
	public JGPoint getImageSize(String imgname) {
		return (JGPoint) image_orig_size.get(imgname);
	}

	/**
	 * Quick version does not scale image on demand, and does not give an error
	 * when image is not defined.
	 */
	public JGImage getImageQuick(String imgname) {
		return (JGImage) images.get(imgname);
	}

	public JGImage getImage(String imgname) {
		if (!existsImage(imgname))
			throw new JGameError("Image '" + imgname + "' not defined.", true);
		JGImage img = (JGImage) images.get(imgname);
		if (img == null) {
			img = (JGImage) images_orig.get(imgname);
			if (img == null)
				return null;
			// convert indexed to display-compatible image
			JGColor render_bg_col = render_bg_color;
			if (render_bg_col == null)
				render_bg_col = bg_color;
			img = img.toDisplayCompatible(alpha_thresh, render_bg_col, true,
					make_bitmask);
			JGPoint size = img.getSize();
			// BufferedImage img2 = JREImage.createCompatibleImage(
			// size.width,size.height, Transparency.TRANSLUCENT );
			// img2.getGraphics().drawImage(img,0,0,null);
			// img=img2;
			if (width > 0 && height > 0) {
				if (prescale) {
					JGPoint scaledpos = scalePos(size.x, size.y, false);
					img = img.scale(scaledpos.x, scaledpos.y);
					// convert translucent image to bitmask
					// not necessary?
					// img = imageutil.toCompatibleBitmask(img,alpha_thresh,
					// render_bg_col,false);
				} // else skip this part for efficiency, even though scalefac
					// is 1.0.
				images.put(imgname, img);
			} else {
				throw new JGameError("Image width, height <= 0 !", true);
			}
		}
		return img;
	}

	/** protected */
	public JGImage getSubImage(String mapname, int imgnr) {
		ImageMap imgmap = (ImageMap) imagemaps.get(mapname);
		if (imgmap == null)
			throw new JGameError("Image map '" + mapname + "' not found.", true);
		JGPoint subcoord = imgmap.getImageCoord(imgnr);
		if (subcoord != null) {
			return imgmap.img.crop(subcoord.x, subcoord.y, imgmap.tilex,
					imgmap.tiley);
		} else {
			return null;
		}
	}

	public void defineMedia(JGEngineInterface eng, String filename) {
		int lnr = 1;
		int nr_lines = 0;
//		filename = getAbsolutePath(eng, filename);
		try {
			FileInputStream instr = new FileInputStream(filename);
			if (instr == null)
				eng.exitEngine("Cannot open `" + filename + "'.");
			InputStreamReader in = new InputStreamReader(instr);
			if (in == null)
				eng.exitEngine("Cannot open `" + filename + "'.");
			// count nr of lines in file first
			while (readline(in) != null)
				nr_lines++;
			if (nr_lines == 0)
				eng.exitEngine("Cannot open `" + filename + "'.");
			// now, read the file
			in = new InputStreamReader(new FileInputStream(filename));
			String line;
			String[] fields = new String[14];
			while ((line = readline(in)) != null) {
				eng.setProgressBar((double) lnr / (double) nr_lines);
				int i = 0;
				Vector tokens = tokenizeString(line, '\t');
				for (Enumeration e = tokens.elements(); e.hasMoreElements();) {
					fields[i++] = (String) e.nextElement();
				}
				if (i == 8) {
					defineImageMap(eng, fields[0], fields[1],
							Integer.parseInt(fields[2]),
							Integer.parseInt(fields[3]),
							Integer.parseInt(fields[4]),
							Integer.parseInt(fields[5]),
							Integer.parseInt(fields[6]),
							Integer.parseInt(fields[7]));
				} else if (i == 9) {
					defineImage(eng, fields[0], fields[1],
							Integer.parseInt(fields[2]), fields[3], fields[4],
							Integer.parseInt(fields[5]),
							Integer.parseInt(fields[6]),
							Integer.parseInt(fields[7]),
							Integer.parseInt(fields[8]));
				} else if (i == 5) {
					defineImage(eng, fields[0], fields[1],
							Integer.parseInt(fields[2]), fields[3], fields[4],
							-1, -1, -1, -1);
				} else if (i == 10) {
					defineImage(
							fields[0],
							fields[1],
							Integer.parseInt(fields[2]),
							getSubImage(fields[3], Integer.parseInt(fields[4])),
							fields[5], Integer.parseInt(fields[6]),
							Integer.parseInt(fields[7]),
							Integer.parseInt(fields[8]),
							Integer.parseInt(fields[9]));
				} else if (i == 6) {
					defineImage(
							fields[0],
							fields[1],
							Integer.parseInt(fields[2]),
							getSubImage(fields[3], Integer.parseInt(fields[4])),
							fields[5], -1, -1, -1, -1);
				} else if (i == 3) {
					defineAnimation(fields[0], splitList(fields[1]),
							Double.parseDouble(fields[2]));
				} else if (i == 4) {
					defineAnimation(fields[0], splitList(fields[1]),
							Double.parseDouble(fields[2]),
							fields[3].equals("true"));
				} else if (i == 2) {
					defineAudioClip(eng, fields[0], fields[1]);
				}
				lnr++;
			}
		} catch (JGameError e) {
			eng.exitEngine("Error in " + filename + " line " + lnr + ": " + e);
		} catch (Exception e) {
			eng.exitEngine("Error in " + filename + " line " + lnr + ":\n"
					+ eng.dbgExceptionToString(e));
		}

	}

	/** Split a ';' separated list of words */
	String[] splitList(String liststr) {
		Vector list = tokenizeString(liststr, ';');
		String[] list_arr = new String[list.size()];
		int i = 0;
		for (Enumeration e = list.elements(); e.hasMoreElements();) {
			list_arr[i] = (String) e.nextElement();
			i++;
		}
		return list_arr;
	}

	/**
	 * Remove all information associated with image, including any cached image
	 * data. Does not unload any image maps. XXX not quite finished; publish
	 * this method when finished.
	 */
	void undefineImage(String name) {
		imageutil.purgeImage((String) images_loaded.get(name));
		images_orig.remove(name);
		image_orig_size.remove(name);
		images.remove(name);
		images_exists.remove(name);
		images_transp.remove(name);
		images_loaded.remove(name);
		images_bbox.remove(name);
		for (int i = bg_images.size() - 1; i >= 0; i--) {
			BGImage bg_image = (BGImage) bg_images.elementAt(i);
			if (bg_image != null && bg_image.imgname.equals(name)) {
				bg_images.setElementAt(null, i);
			}
		}
		// XXX association with tile is not yet removed; as we cannot obtain
		// the tile name from the image name
		// Integer tileid = new Integer(tilestrToInt(tilename));
		// Hashtable images_tile = new Hashtable(); /* Integers -> Strings */
		// Hashtable images_tilecid = new Hashtable(); /* Integers -> Integers
		// */
	}

	public void defineImage(Object pkg_obj, String name, String tilename,
			int collisionid, String imgfile, String img_op, int top, int left,
			int width, int height) {
		if (images_loaded.containsKey(name)) {
			undefineImage(name);
		}
		JGImage img = null;
		if (!imgfile.equals("null")) {
//			imgfile = getAbsolutePath(pkg_obj, imgfile);
			img = imageutil.loadImage(imgfile);
			images_loaded.put(name, imgfile);
		}
		defineImage(name, tilename, collisionid, img, img_op, top, left, width,
				height);
	}

	/**
	 * passing -1 to top,left,width,height indicates these have to be taken from
	 * the image dimensions.
	 */
	public void defineImage(String name, String tilename, int collisionid,
			JGImage img, String img_op, int top, int left, int width, int height) {
		if (img != null) {
			/* do image ops */
			img_op = img_op.toLowerCase();
			boolean flipx = img_op.indexOf("x") >= 0;
			boolean flipy = img_op.indexOf("y") >= 0;
			boolean rot90 = img_op.indexOf("r") >= 0;
			boolean rot180 = img_op.indexOf("u") >= 0;
			boolean rot270 = img_op.indexOf("l") >= 0;
			// System.out.println("img_op "+img_op+ " "+flipx+" "+flipy);
			if (flipx || flipy)
				img = img.flip(flipx, flipy);
			if (rot90) {
				img = img.rotate(90);
			} else if (rot180) {
				img = img.rotate(180);
			} else if (rot270) {
				img = img.rotate(270);
			}
			images_orig.put(name, img);
			image_orig_size.put(name, img.getSize());
		}
		images_exists.put(name, "yes");
		Integer tileid = new Integer(tilestrToInt(tilename));
		if (img == null || !img.isOpaque(alpha_thresh))
			images_transp.put(tileid, "yes");
		images_tile.put(tileid, name);
		images_tilecid.put(tileid, new Integer(collisionid));
		// width/height < 0 indicate take bounding box from image dims
		if (width >= 0) {
			images_bbox.put(name, new JGRectangle(top, left, width, height));
		} else {
			JGPoint size;
			if (img == null)
				size = new JGPoint(0, 0);
			else
				size = img.getSize();
			images_bbox.put(name, new JGRectangle(0, 0, size.x, size.y));
		}
		getImage(name); /* pre-load scaled image */
		// throw away unscaled image to save memory
		// XXX remove images_orig altogether
		images_orig.remove(name);
	}

	public void defineImageMap(Object pkg_obj, String mapname, String imgfile,
			int xofs, int yofs, int tilex, int tiley, int skipx, int skipy) {
//		imgfile = getAbsolutePath(pkg_obj, imgfile);
		imagemaps.put(mapname, new ImageMap(imageutil, imgfile, xofs, yofs,
				tilex, tiley, skipx, skipy));
	}

	public JGRectangle getImageBBox(String imgname) {
		return (JGRectangle) images_bbox.get(imgname);
	}

	/* ====== image from engine ====== */

	public void defineImage(Object pkg_obj, String imgname, String tilename,
			int collisionid, String imgfile, String img_op) {
		defineImage(pkg_obj, imgname, tilename, collisionid, imgfile, img_op,
				-1, -1, -1, -1);
	}

	public void defineImage(String imgname, String tilename, int collisionid,
			String imgmap, int mapidx, String img_op, int top, int left,
			int width, int height) {
		defineImage(imgname, tilename, collisionid,
				getSubImage(imgmap, mapidx), img_op, top, left, width, height);
	}

	public void defineImage(String imgname, String tilename, int collisionid,
			String imgmap, int mapidx, String img_op) {
		defineImage(imgname, tilename, collisionid,
				getSubImage(imgmap, mapidx), img_op, 0, 0, -1, -1);
	}

	/* ====== PF/view ====== */

	/**
	 * Offset that should be set on next frame draw. Offset is clipped so the
	 * view always fits within playfield.
	 */
	void setPendingViewOffset(int xofs, int yofs) {
		if (!pf_wrapx) {
			if (xofs < 0)
				xofs = 0;
			if (xofs > tilex * (nrtilesx - viewnrtilesx))
				xofs = tilex * (nrtilesx - viewnrtilesx);
		}
		if (!pf_wrapy) {
			if (yofs < 0)
				yofs = 0;
			if (yofs > tiley * (nrtilesy - viewnrtilesy))
				yofs = tiley * (nrtilesy - viewnrtilesy);
		}
		pendingxofs = xofs;
		pendingyofs = yofs;
		// update parallax level 0
		if (bg_images.size() >= 1) {
			BGImage bgimg = (BGImage) bg_images.elementAt(0);
			if (bgimg != null) {
				bgimg.xofs = xofs;
				bgimg.yofs = yofs;
			}
		}
	}

	void setPendingView(int xofs, int yofs) {
		pendingxofs = xofs;
		pendingyofs = yofs;
		// update parallax level 0
		if (bg_images.size() >= 1) {
			BGImage bgimg = (BGImage) bg_images.elementAt(0);
			if (bgimg != null) {
				bgimg.xofs = xofs;
				bgimg.yofs = yofs;
			}
		}
	}

	/** Update offset according to pending offset. Protected. */
	public void updateViewOffset() {
		if (pendingxofs != xofs || pendingyofs != yofs) {
			setViewOffset(pendingxofs, pendingyofs);
		}
	}

	/** Update all offset-related variables according to given offset. */
	void setViewOffset(int xofs, int yofs) {
		if (bg_defined == null)
			return;
		this.xofs = xofs;
		this.yofs = yofs;
		int oldtilexofs = tilexofs;
		int oldtileyofs = tileyofs;
		tilexofs = divFloor(xofs, tilex) - 1;
		tileyofs = divFloor(yofs, tiley) - 1;
		// XXX does scalePos handle negative numbers properly?
		xofs_scaled = scaleXPos(xofs, false);
		yofs_scaled = scaleYPos(yofs, false);
		calcPFWrapCenter();
		int maxtilexofs = Math.max(tilexofs, oldtilexofs);
		int maxtileyofs = Math.max(tileyofs, oldtileyofs);
		int mintilexofs = Math.min(tilexofs, oldtilexofs);
		int mintileyofs = Math.min(tileyofs, oldtileyofs);
		// bg_defined = new boolean[viewnrtilesx+3][viewnrtilesy+3];
		// wipe all rows and columns that the vector
		// oldtileofs -> tileofs crosses
		for (int yi = mintileyofs; yi < maxtileyofs; yi++) {
			for (int xi = 0; xi < viewnrtilesx + 3; xi++) {
				bg_defined[xi][moduloFloor(yi, viewnrtilesy + 3)] = false;
			}
		}
		for (int xi = mintilexofs; xi < maxtilexofs; xi++) {
			for (int yi = 0; yi < viewnrtilesy + 3; yi++) {
				bg_defined[moduloFloor(xi, viewnrtilesx + 3)][yi] = false;
			}
		}
		// bg_defined[xi%(viewnrtilesx+3)][yi%(viewnrtilesy+3)]=true;
	}

	void calcPFWrapCenter() {
		xofs_mid = xofs + viewnrtilesx * tilex / 2 + pf_wrapshiftx;
		yofs_mid = yofs + viewnrtilesy * tiley / 2 + pf_wrapshifty;
	}

	/* ====== objects from canvas ====== */

	public void markAddObject(JGObject obj) {
		obj_to_add.put(obj.getName(), obj);
	}

	/**
	 * Add new object now. Old object with the same name is replaced
	 * immediately, and its remove() method called.
	 */
	void addObject(JGObject obj) {
		int idx = objects.get(obj.getName());
		if (idx >= 0) {
			JGObject old_obj = (JGObject) objects.values[idx];
			// disable object so it doesn't call engine on removal
			old_obj.removeDone();
			// ensure any dispose stuff in the object is called
			old_obj.remove();
		}
		objects.put(obj.getName(), obj);
	}

	/** Mark object for removal. */
	void markRemoveObject(String index) {
		int idx = objects.get(index);
		if (idx < 0)
			return;
		obj_to_remove.put(index, (JGObject) objects.values[idx]);
	}

	/** Mark object for removal. */
	void markRemoveObject(JGObject obj) {
		obj_to_remove.put(obj.getName(), obj);
	}

	/** Actually remove object now */
	void doRemoveObject(JGObject obj) {
		obj.removeDone();
		objects.remove(obj.getName());
	}

	/** Mark all objects with given spec for removal. */
	void markRemoveObjects(String prefix, int cidmask, boolean suspended_obj) {
		obj_spec_to_remove.addElement(prefix);
		obj_spec_to_remove.addElement(new Integer(cidmask));
		obj_spec_to_remove.addElement(new Boolean(suspended_obj));
	}

	/**
	 * Actually remove objects with given spec, including those in obj_to_add
	 * list. Uses obj_to_remove as a temp variable. If anything is already in
	 * obj_to_remove, it is left there. If do_remove_list is true, the objects
	 * are removed and obj_to_remove is cleared. Otherwise, the objects to
	 * remove are just added to obj_to_remove.
	 */
	void doRemoveObjects(String prefix, int cidmask, boolean suspended_obj,
			boolean do_remove_list) {
		int firstidx = getFirstObjectIndex(prefix);
		int lastidx = getLastObjectIndex(prefix);
		for (int i = firstidx; i < lastidx; i++) {
			JGObject o = (JGObject) objects.values[i];
			if (cidmask == 0 || (o.colid & cidmask) != 0) {
				if (suspended_obj || !o.is_suspended) {
					obj_to_remove.put(objects.keys[i], o);
				}
			}
		}
		if (do_remove_list)
			doRemoveList();
		// if we enumerate backwards, we can remove elements inline without
		// consistency problems
		for (int i = obj_to_add.size - 1; i >= 0; i--) {
			JGObject o = (JGObject) obj_to_add.values[i];
			if (prefix == null || obj_to_add.keys[i].startsWith(prefix)) {
				if (cidmask == 0 || (o.colid & cidmask) != 0) {
					if (suspended_obj || !o.is_suspended) {
						// Note: remove element inside element enumeration
						obj_to_add.remove(obj_to_add.keys[i]);
					}
				}
			}
		}
	}

	/** protected, remove objects marked for removal. */
	public void flushRemoveList() {
		// for (Enumeration e=obj_to_remove.elements(); e.hasMoreElements();) {
		// String name = (String)e.nextElement();
		// JGObject o = (JGObject)objects.get(name);
		// if (o!=null) { // object might have been removed already
		// doRemoveObject(o);
		// }
		// }
		// add all query results from object specs to obj_to_remove
		// don't enumerate when no elements (which is about 90% of the time)
		if (obj_spec_to_remove.size() != 0) {
			for (Enumeration e = obj_spec_to_remove.elements(); e
					.hasMoreElements();) {
				String prefix = (String) e.nextElement();
				int cid = ((Integer) e.nextElement()).intValue();
				boolean suspended_obj = ((Boolean) e.nextElement())
						.booleanValue();
				doRemoveObjects(prefix, cid, suspended_obj, false);
			}
			obj_spec_to_remove.removeAllElements();
		}
		// remove everything in one go
		doRemoveList();
	}

	/** Actually remove objects in obj_to_remove. */
	void doRemoveList() {
		for (int i = 0; i < obj_to_remove.size; i++) {
			((JGObject) obj_to_remove.values[i]).removeDone();
		}
		objects.remove(obj_to_remove);
		obj_to_remove.clear();
	}

	/**
	 * Add objects marked for addition. Protected.
	 */
	public void flushAddList() {
		// XXX we have to add one by one because we have to call the dispose
		// method of the objects that are replaced
		for (int i = 0; i < obj_to_add.size; i++) {
			addObject((JGObject) obj_to_add.values[i]);
		}
		obj_to_add.clear();
	}

	// /** Remove objects marked for addition before they can be added.
	// * Protected.
	// */
	// public void clearAddList() {
	// for (int i=0; i<obj_to_add.size; i++) {
	// // be sure to mark the object as removed
	// ((JGObject)obj_to_add.values[i]).removeDone();
	// }
	// obj_to_add.clear();
	// }

	/* public */

	public boolean existsObject(String index) {
		return objects.get(index) >= 0;
	}

	public JGObject getObject(String index) {
		int idx = objects.get(index);
		if (idx < 0)
			return null;
		return (JGObject) objects.values[idx];
	}

	// /** Remove all objects. All objects are marked for removal, the add
	// * list is cleared. */
	// void clearObjects() {
	// for (Enumeration e=objects.keys(); e.hasMoreElements(); ) {
	// markRemoveObject((String)e.nextElement());
	// }
	// clearAddList();
	// //clear_objects=true;
	// }

	public void moveObjects(JGEngineInterface eng, String prefix, int cidmask) {
		if (in_parallel_upd)
			throw new JGameError("Recursive call", true);
		in_parallel_upd = true;
		int firstidx = getFirstObjectIndex(prefix);
		int lastidx = getLastObjectIndex(prefix);
		for (int i = firstidx; i < lastidx; i++) {
			JGObject o = (JGObject) objects.values[i];
			if (cidmask != 0 && (o.colid & cidmask) == 0)
				continue;
			// first, update suspend mode
			if (o.is_suspended) {
				if (o.resume_in_view
						&& o.isInView(offscreen_margin_x, offscreen_margin_y))
					o.resume();
			} else {
				if (o.expiry == JGObject.suspend_off_view
						|| o.expiry == JGObject.suspend_off_view_expire_off_pf) {
					if (!o.isInView(offscreen_margin_x, offscreen_margin_y))
						o.suspend();
				}
			}
			// move object
			// we must ensure modulo is correct when object is suspended so
			// that it will unsuspend properly
			// o.moduloPos(); // is inlined below
			if (pf_wrapx)
				o.x = moduloXPos(o.x);
			if (pf_wrapy)
				o.y = moduloYPos(o.y);
			if (!o.is_suspended) {
				// o.moduloPos(); // is inlined below
				if (pf_wrapx)
					o.x = moduloXPos(o.x);
				if (pf_wrapy)
					o.y = moduloYPos(o.y);
				try {
					o.move();
				} catch (JGameError ex) {
					eng.exitEngine(eng.dbgExceptionToString(ex));
				} catch (Exception ex) {
					eng.dbgShowException(o.getName(), ex);
				}
				o.updateAnimation(gamespeed);
				o.x += o.xdir * o.xspeed * gamespeed;
				o.y += o.ydir * o.yspeed * gamespeed;
				// o.moduloPos(); // is inlined below
				if (pf_wrapx)
					o.x = moduloXPos(o.x);
				if (pf_wrapy)
					o.y = moduloYPos(o.y);
			}
			// check expiry; object should not expire when suspended
			if (!o.is_suspended) {
				int expiry = (int) o.expiry;
				if (expiry >= 0) {
					o.expiry -= gamespeed;
					if (o.expiry < 0)
						o.remove();
				} else {
					if (expiry == JGObject.expire_off_pf
							|| expiry == JGObject.suspend_off_view_expire_off_pf) {
						if (!o.isOnPF(offscreen_margin_x, offscreen_margin_y))
							o.remove();
					}
					if (expiry == JGObject.expire_off_view
							&& !o.isInView(offscreen_margin_x,
									offscreen_margin_y))
						o.remove();
				}
			}
		}
		flushRemoveList();
		in_parallel_upd = false;
	}

	public void moveObjects(JGEngineInterface eng) {
		moveObjects(eng, null, 0);
	}

	/* reused rectangles, used within collision methods */

	JGRectangle tmprect1 = new JGRectangle();
	JGRectangle tmprect2 = new JGRectangle();

	JGObject[] srcobj = new JGObject[50];
	JGObject[] dstobj = new JGObject[50];

	public void checkCollision(JGEngineInterface eng, int srccid, int dstcid) {
		if (in_parallel_upd)
			throw new JGameError("Recursive call", true);
		in_parallel_upd = true;
		if (objects.size > srcobj.length) {
			// grow arrays to make objects fit
			srcobj = new JGObject[objects.size + 50];
			dstobj = new JGObject[objects.size + 50];
		}
		int srcsize = 0;
		int dstsize = 0;
		/* get all matching objects */
		JGRectangle sr = tmprect1;
		JGRectangle dr = tmprect2;
		for (int i = 0; i < objects.size; i++) {
			JGObject o = (JGObject) objects.values[i];
			if (o.is_suspended)
				continue;
			if (!o.getBBox(sr))
				continue;
			if ((o.colid & srccid) != 0) {
				srcobj[srcsize++] = o;
			}
			if ((o.colid & dstcid) != 0) {
				dstobj[dstsize++] = o;
			}
		}
		/* check collision */
		for (int si = 0; si < srcsize; si++) {
			JGObject srco = srcobj[si];
			if (!srco.getBBox(sr))
				continue;
			for (int di = 0; di < dstsize; di++) {
				JGObject dsto = dstobj[di];
				if (dsto == srco)
					continue;
				if (!dsto.getBBox(dr))
					continue;
				if (sr.intersects(dr)) {
					try {
						dsto.hit(srco);
					} catch (JGameError ex) {
						eng.exitEngine(eng.dbgExceptionToString(ex));
					} catch (Exception ex) {
						eng.dbgShowException(dsto.getName(), ex);
					}
				}
			}
		}
		flushRemoveList();
		in_parallel_upd = false;
	}

	public int checkCollision(int cidmask, JGObject obj) {
		JGRectangle bbox = obj.getBBox();
		if (bbox == null)
			return 0;
		int retcid = 0;
		JGRectangle obj_bbox = tmprect1;
		for (int i = 0; i < objects.size; i++) {
			JGObject o = (JGObject) objects.values[i];
			if (o == obj)
				continue;
			if (!o.is_suspended) {
				if (cidmask == 0 || (o.colid & cidmask) != 0) {
					if (!o.getBBox(obj_bbox))
						continue;
					if (bbox.intersects(obj_bbox)) {
						retcid |= o.colid;
					}
				}
			}
		}
		return retcid;
	}

	public int checkBGCollision(JGRectangle r) {
		return getTileCid(getTiles(r));
	}

	public void checkBGCollision(JGEngineInterface eng, int tilecid, int objcid) {
		if (in_parallel_upd)
			throw new JGameError("Recursive call", true);
		in_parallel_upd = true;
		if (objects.size > srcobj.length) {
			// grow arrays to make objects fit
			srcobj = new JGObject[objects.size + 50];
		}
		int srcsize = 0;
		JGRectangle r = tmprect1;
		/* get all matching objects */
		for (int i = 0; i < objects.size; i++) {
			JGObject o = (JGObject) objects.values[i];
			if (o.is_suspended)
				continue;
			if (!o.getTileBBox(r))
				continue;
			if ((o.colid & objcid) != 0) {
				srcobj[srcsize++] = o;
			}
		}
		/* check collision */
		JGRectangle tiler = tmprect2;
		for (int i = 0; i < srcsize; i++) {
			JGObject o = srcobj[i];
			// tile bbox is always defined
			o.getTileBBox(r);
			// fast equivalent of cid=checkBGCollision(r)
			getTiles(tiler, r);
			int cid = getTileCid(tiler);
			if ((cid & tilecid) != 0) {
				try {
					o.hit_bg(cid);
					o.hit_bg(cid, tiler.x, tiler.y, tiler.width, tiler.height);
					// XXX this might be slow, check its performance
					for (int y = 0; y < tiler.height; y++) {
						for (int x = 0; x < tiler.width; x++) {
							int thiscid = getTileCid(tiler.x + x, tiler.y + y);
							if ((thiscid & tilecid) != 0)
								o.hit_bg(thiscid, tiler.x + x, tiler.y + y);
						}
					}
				} catch (JGameError ex) {
					eng.exitEngine(eng.dbgExceptionToString(ex));
				} catch (Exception ex) {
					eng.dbgShowException(o.getName(), ex);
				}
			}
		}
		flushRemoveList();
		in_parallel_upd = false;
	}

	/* objects from engine */

	public Vector getObjects(String prefix, int cidmask, boolean suspended_obj,
			JGRectangle bbox) {
		Vector objects_v = new Vector(50, 100);
		int nr_obj = 0;
		JGRectangle obj_bbox = tmprect1;
		int firstidx = getFirstObjectIndex(prefix);
		int lastidx = getLastObjectIndex(prefix);
		for (int i = firstidx; i < lastidx; i++) {
			JGObject obj = (JGObject) objects.values[i];
			if (cidmask == 0 || (obj.colid & cidmask) != 0) {
				if (suspended_obj || !obj.is_suspended) {
					if (bbox != null) {
						if (!obj.getBBox(obj_bbox))
							continue;
						if (bbox.intersects(obj_bbox)) {
							objects_v.addElement(obj);
						}
					} else {
						objects_v.addElement(obj);
					}
				}
			}
		}
		return objects_v;
	}

	public void removeObject(JGObject obj) {
		if (in_parallel_upd) { // queue remove
			markRemoveObject(obj);
		} else { // do remove immediately
			doRemoveObject(obj);
		}
	}

	public void removeObjects(String prefix, int cidmask) {
		removeObjects(prefix, cidmask, true);
	}

	public void removeObjects(String prefix, int cidmask, boolean suspended_obj) {
		if (in_parallel_upd) {
			markRemoveObjects(prefix, cidmask, suspended_obj);
		} else {
			doRemoveObjects(prefix, cidmask, suspended_obj, true);
		}
	}

	public int countObjects(String prefix, int cidmask) {
		return countObjects(prefix, cidmask, true);
	}

	public int countObjects(String prefix, int cidmask, boolean suspended_obj) {
		int nr_obj = 0;
		int firstidx = getFirstObjectIndex(prefix);
		int lastidx = getLastObjectIndex(prefix);
		for (int i = firstidx; i < lastidx; i++) {
			JGObject obj = (JGObject) objects.values[i];
			if (cidmask == 0 || (obj.colid & cidmask) != 0) {
				if (suspended_obj || !obj.is_suspended) {
					nr_obj++;
				}
			}
		}
		return nr_obj;
	}

	int getFirstObjectIndex(String prefix) {
		if (prefix == null)
			return 0;
		int firstidx = objects.get(prefix);
		if (firstidx < 0)
			firstidx = -1 - firstidx;
		return firstidx;
	}

	int getLastObjectIndex(String prefix) {
		if (prefix == null)
			return objects.size;
		// XXX theoretically there may be strings with prefix
		// lexicographically below this one
		return -1 - objects.get(prefix + '\uffff');
	}

	/**
	 * Do final update actions on objects after all frame updates finished.
	 * Protected.
	 */
	public void frameFinished() {
		for (int i = 0; i < objects.size; i++) {
			((JGObject) objects.values[i]).frameFinished();
		}
	}

	/* ====== BG/tiles ====== */

	/**
	 * Repaint those parts of BG which are undefined according to bg_defined.
	 * Handles wraparound if applicable. If wraparound is on, xofs and yofs may
	 * be any value.
	 */
	public void repaintBG(JGEngineInterface eng) {
		if (bg_defined == null)
			return;
		// first, convert tilechangedmap entries to bg_defined entries. Note
		// that one tilechanged may result in multiple bg undefineds, in case
		// the playfield is smaller than viewnrtiles+3.
		// for (int x=0; x<viewnrtilesx+3; x++) {
		// int xi = x + tilexofs;
		// // check if out of playfield bounds, only needed if not wrapping.
		// if (!pf_wrapx && (xi<0 || xi>=nrtilesx)) continue;
		// int xi_mod = moduloFloor(xi,viewnrtilesx+3);
		// int xi_modpf = xi;
		// if (pf_wrapx) xi_modpf = moduloFloor(xi,nrtilesx);
		// // determine loop bounds for tight loop
		// int ylower = tileyofs;
		// int yupper = tileyofs + viewnrtilesy+3;
		// if (!pf_wrapy) {
		// if (ylower<0) ylower=0;
		// if (yupper<0) continue;
		// if (ylower>nrtilesy) continue;
		// if (yupper>nrtilesy) yupper=nrtilesy; // exclusive bound
		// }
		// for (int yi=ylower; yi<yupper; yi++) {
		// // check if out of playfield bounds: already done
		// //if (!pf_wrapy && (yi<0 || yi>=nrtilesy)) continue;
		// // check if already defined
		// int yi_modpf = yi;
		// if (pf_wrapy)
		// yi_modpf = moduloFloor(yi,nrtilesy);
		// if (!tilechangedmap[xi_modpf][yi_modpf]) continue;
		// int yi_mod = moduloFloor(yi,viewnrtilesy+3);
		// bg_defined[xi_mod][yi_mod]=false;
		// }
		// }
		int nrtilesdrawn = 0;
		// The pre-draw area is the 1-tile thick space surrounding the visible
		// area, which does not really need to be drawn. It is used to
		// pre-draw tiles before they become visible, so that the system can
		// draw tiles a few at a time when scrolling slowly. Otherwise it
		// would have to wait for the view offset to cross a tile boundary, at
		// which point it would have to draw a whole row/column of tiles at a
		// time.
		// int nrpredrawtilesdrawn=0;
		// The pre-draw quota is the number of tiles that the system should
		// draw in the pre-draw area when it has nothing to do.
		// Upper bound for pre-draw quota is a whole row plus column divided
		// by 2 (so that the work of a diagonal scroll is split into at least
		// 2 equal batches). A better estimate for the optimal quota may be
		// achieved by estimating the expected scroll speed. For now we
		// choose a default of (rows+columns)/5.
		int maxpredrawtiles = (viewnrtilesx + viewnrtilesy) / 5;
		for (int x = 0; x < viewnrtilesx + 3; x++) {
			int xi = x + tilexofs;
			// check if out of playfield bounds, only needed if not wrapping.
			if (!pf_wrapx && (xi < 0 || xi >= nrtilesx))
				continue;
			int xi_mod = moduloFloor(xi, viewnrtilesx + 3);
			int xi_modpf = xi;
			if (pf_wrapx)
				xi_modpf = moduloFloor(xi, nrtilesx);
			// determine loop bounds for tight loop
			int ylower = tileyofs;
			int yupper = tileyofs + viewnrtilesy + 3;
			if (!pf_wrapy) {
				if (yupper < 0)
					continue;
				if (ylower < 0)
					ylower = 0;
				if (ylower > nrtilesy)
					continue;
				if (yupper > nrtilesy)
					yupper = nrtilesy; // exclusive bound
			}
			int divfactor = viewnrtilesy + 3;
			for (int yi = ylower; yi < yupper; yi++) {
				// check if out of playfield bounds: already done
				// if (!pf_wrapy && (yi<0 || yi>=nrtilesy)) continue;
				// check if already defined
				// int yi_mod = moduloFloor(yi,divfactor);
				// version with moduloFloor inlined
				int yi_mod = (yi >= 0) ? yi % divfactor : divfactor - 1
						- ((-1 - yi) % divfactor);
				if (bg_defined[xi_mod][yi_mod])
					continue;
				int yi_modpf = yi;
				if (pf_wrapy)
					yi_modpf = moduloFloor(yi, nrtilesy);
				// && !tilechangedmap[xi_modpf][yi_modpf]) continue;
				int y = yi - tileyofs;
				if (x == 0 || y == 0 || x == viewnrtilesx + 2
						|| y == viewnrtilesy + 2) {
					// tile is inside the pre-draw area.
					// don't draw if we already reached quota.
					if (nrtilesdrawn > maxpredrawtiles)
						continue;
					// nrpredrawtilesdrawn++;
				}
				eng.drawTile(xi, yi, tilemap[xi_modpf][yi_modpf]);
				nrtilesdrawn++;
				bg_defined[xi_mod][yi_mod] = true;
				// tilechangedmap[xi_modpf][yi_modpf]=false;
			}
		}
		// System.out.println("Nr. tiles updated: "+nrtilesdrawn);
	}

	public void setBGImage(String bgimg, int depth, boolean wrapx, boolean wrapy) {
		while (bg_images.size() < depth + 1)
			bg_images.addElement(null);
		if (bgimg != null) {
			if (!images.containsKey(bgimg))
				throw new JGameError("unknown BG image " + bgimg);
			BGImage newimg = new BGImage(bgimg, wrapx, wrapy);
			bg_images.setElementAt(newimg, depth);
			if (depth == 0) {
				newimg.xofs = xofs;
				newimg.yofs = yofs;
			}
		} else {
			bg_images.setElementAt(null, depth);
		}
	}

	public boolean removeBGImage(String name) {
		System.out.println(bg_images.indexOf(name));
		return bg_images.removeElement(name);
	}

	/** protected */
	public void initBGTiles(int nrtilesx, int nrtilesy, String filltile) {
		this.nrtilesx = nrtilesx;
		this.nrtilesy = nrtilesy;
		pfwidth = nrtilesx * tilex;
		pfheight = nrtilesy * tiley;
		pfwidth_half = pfwidth / 2;
		pfheight_half = pfheight / 2;
		tilemap = new int[nrtilesx][nrtilesy];
		tilecidmap = new int[nrtilesx][nrtilesy];
		// tilechangedmap = new boolean [nrtilesx][nrtilesy];
		bg_defined = new boolean[viewnrtilesx + 3][viewnrtilesy + 3];
		fillBG(filltile);
		setViewOffset(0, 0);
	}

	/** protected */
	public void invalidateBGTiles() {
		if (bg_defined == null)
			return;
		for (int x = 0; x < viewnrtilesx + 3; x++) {
			for (int y = 0; y < viewnrtilesy + 3; y++) {
				bg_defined[x][y] = false;
			}
		}
	}

	public void setTileSettings(String out_of_bounds_tile,
			int out_of_bounds_cid, int preserve_cids) {
		this.out_of_bounds_tile = out_of_bounds_tile;
		this.out_of_bounds_cid = out_of_bounds_cid;
		this.preserve_cids = preserve_cids;
	}

	public void fillBG(String filltile) {
		for (int y = 0; y < nrtilesy; y++) {
			for (int x = 0; x < nrtilesx; x++) {
				setTile(x, y, filltile);
			}
		}
	}

	public void setTileCid(int x, int y, int and_mask, int or_mask) {
		// inlined tile out of bounds handling
		if (pf_wrapx) {
			x = moduloFloor(x, nrtilesx);
		} else {
			if (x < 0 || x >= nrtilesx)
				return;
		}
		if (pf_wrapy) {
			y = moduloFloor(y, nrtilesy);
		} else {
			if (y < 0 || y >= nrtilesy)
				return;
		}
		tilecidmap[x][y] &= and_mask;
		tilecidmap[x][y] |= or_mask;
	}

	public void setTile(int x, int y, String tilestr) {
		// inlined tile out of bounds handling
		int x_mod = x, y_mod = y;
		if (pf_wrapx) {
			x_mod = moduloFloor(x, nrtilesx);
		} else {
			if (x < 0 || x >= nrtilesx)
				return;
		}
		if (pf_wrapy) {
			y_mod = moduloFloor(y, nrtilesy);
		} else {
			if (y < 0 || y >= nrtilesy)
				return;
		}
		int tileid = tilestrToInt(tilestr);
		tilemap[x_mod][y_mod] = tileid;
		tilecidmap[x_mod][y_mod] &= preserve_cids;
		tilecidmap[x_mod][y_mod] |= tileintToCid(tileid);
		// invalidate tile, to be redrawn later
		// new method of invalidating tile; the tilechanged flag is picked
		// up by repaintBG.
		// tilechangedmap[x_mod][y_mod] = true;

		// update bg_defined
		// check if we are in view
		// XXX for pf_wrap we also need to modulo tilex/yofs and reverse
		// bounds if necessary. For now we just invalidate too many tiles.
		if (!pf_wrapx)
			if (x_mod < tilexofs || x_mod >= tilexofs + viewnrtilesx + 3)
				return;
		if (!pf_wrapy)
			if (y_mod < tileyofs || y_mod >= tileyofs + viewnrtilesy + 3)
				return;
		// find offset in bg
		// int x_mod_bg = moduloFloor(x,viewnrtilesx+3);
		// int y_mod_bg = moduloFloor(y,viewnrtilesy+3);
		// version with moduloFloor inlined
		int x_mod_bg = (x >= 0) ? x % (viewnrtilesx + 3) : viewnrtilesx + 2
				- ((-1 - x) % (viewnrtilesx + 3));
		int y_mod_bg = (y >= 0) ? y % (viewnrtilesy + 3) : viewnrtilesy + 2
				- ((-1 - y) % (viewnrtilesy + 3));
		// in case we wrap and bg is larger than pf, ensure we are within
		// 0 < x_mod_bg < nrtilesx
		if (x_mod_bg >= nrtilesx)
			x_mod_bg -= nrtilesx;
		if (y_mod_bg >= nrtilesy)
			y_mod_bg -= nrtilesy;
		// invalidate tile
		bg_defined[x_mod_bg][y_mod_bg] = false;
		// see if we need to undefine multiple tiles, in case of wrap-around
		// and playfield size smaller than bg size
		boolean do_doubley = false;
		if (pf_wrapx) {
			if (viewnrtilesx + 3 - nrtilesx > x_mod_bg) {
				bg_defined[x_mod_bg + nrtilesx][y_mod_bg] = false;
				if (pf_wrapy) {
					if (viewnrtilesy + 3 - nrtilesy > y_mod_bg) {
						bg_defined[x_mod_bg + nrtilesx][y_mod_bg + nrtilesy] = false;
						do_doubley = true;
					}
				}
			}
		}
		if (do_doubley) {
			bg_defined[x_mod_bg][y_mod_bg + nrtilesy] = false;
		}
		// old routine, which finds whether it's in the viewport but fails to
		// easily find the appropriate bg_defined entry
		/*
		 * if (pf_wrapx) { int bg_x1_mod = moduloFloor(tilexofs-1,nrtilesx); int
		 * bg_x2_mod = moduloFloor(tilexofs+viewnrtilesx+2,nrtilesx); if
		 * (bg_x1_mod < bg_x2_mod) { if (x_mod < bg_x1_mod || x_mod >=
		 * bg_x2_mod) return; } else { if (x_mod < bg_x1_mod && x_mod >=
		 * bg_x2_mod) return; } } else { if ((x-tilexofs < -1) || (x-tilexofs >=
		 * viewnrtilesx+2) ) return; } if (pf_wrapy) { int bg_y1_mod =
		 * moduloFloor(tileyofs-1,nrtilesy); int bg_y2_mod =
		 * moduloFloor(tileyofs+viewnrtilesy+2,nrtilesy); if (bg_y1_mod <
		 * bg_y2_mod) { if (y_mod < bg_y1_mod || y_mod >= bg_y2_mod) return; }
		 * else { if (y_mod < bg_y1_mod && y_mod >= bg_y2_mod) return; } } else
		 * { if ((y-tileyofs < -1) || (y-tileyofs >= viewnrtilesy+2)) return; }
		 */
		// System.out.print("/x"+x+"y"+y);
		// XXX modulo is wrong!
		// x = moduloFloor(x+1, viewnrtilesx+3);
		// y = moduloFloor(y+1, viewnrtilesy+3);
		// System.out.print(" x"+x+"y"+y);
		// bg_defined[x][y] = false;
		// XXX these are not yet adapted to wraparound
		// XXX this routine does not work for scrolling!
		// if ((x-tilexofs < 0)
		// || (x-tilexofs >= viewnrtilesx+3) ) return;
		// if ((y-tileyofs < 0)
		// || (y-tileyofs >= viewnrtilesy+3)) return;
		// drawTile(x,y,tileid);
	}

	public int countTiles(int tilecidmask) {
		int count = 0;
		for (int x = 0; x < nrtilesx; x++) {
			for (int y = 0; y < nrtilesy; y++) {
				if ((tilecidmap[x][y] & tilecidmask) != 0)
					count++;
			}
		}
		return count;
	}

	public int getTileCid(int xidx, int yidx) {
		if (pf_wrapx) {
			xidx = moduloFloor(xidx, nrtilesx);
		} else {
			if (xidx < 0 || xidx >= nrtilesx)
				return out_of_bounds_cid;
		}
		if (pf_wrapy) {
			yidx = moduloFloor(yidx, nrtilesy);
		} else {
			if (yidx < 0 || yidx >= nrtilesy)
				return out_of_bounds_cid;
		}
		return tilecidmap[xidx][yidx];
	}

	public String getTileStr(int xidx, int yidx) {
		if (pf_wrapx) {
			xidx = moduloFloor(xidx, nrtilesx);
		} else {
			if (xidx < 0 || xidx >= nrtilesx)
				return out_of_bounds_tile;
		}
		if (pf_wrapy) {
			yidx = moduloFloor(yidx, nrtilesy);
		} else {
			if (yidx < 0 || yidx >= nrtilesy)
				return out_of_bounds_tile;
		}
		return tileidToString(tilemap[xidx][yidx]);
	}

	public int getTileCid(JGRectangle tiler) {
		int cid = 0;
		for (int x = tiler.x; x < tiler.x + tiler.width; x++)
			for (int y = tiler.y; y < tiler.y + tiler.height; y++)
				cid |= getTileCid(x, y);
		return cid;
	}

	private int tilestrToCid(String tilestr) {
		if (tilestr == null || tilestr.length() == 0)
			return 0;
		Integer tileid = (Integer) images_tilecid.get(new Integer(
				tilestrToInt(tilestr)));
		if (tileid == null) {
			System.out.println("Warning: unknown tile '" + tilestr + "'.");
			// XXX no reference to dbg
			// dbgPrint("MAIN","Warning: unknown tile '"+tilestr+"'.");
			return 0;
		}
		return tileid.intValue();
	}

	private int tileintToCid(int tileint) {
		if (tileint == 0)
			return 0;
		Integer tileid = (Integer) images_tilecid.get(new Integer(tileint));
		if (tileid == null) {
			System.out.println("Warning: unknown tile '" + tileint + "'.");
			// XXX no reference to dbg
			// dbgPrint("MAIN","Warning: unknown tile '"+tileint+"'.");
			return 0;
		}
		return tileid.intValue();
	}

	/** null or empty string -&gt; 0 */
	private int tilestrToInt(String tilestr) {
		if (tilestr == null)
			return 0;
		switch (tilestr.length()) {
		case 0:
			return 0;
		case 1:
			return (int) tilestr.charAt(0);
		case 2:
			return (int) tilestr.charAt(0) + (int) tilestr.charAt(1) * 256;
		case 3:
			return (int) tilestr.charAt(0) + (int) tilestr.charAt(1) * 256
					+ (int) tilestr.charAt(2) * 256 * 256;
		case 4:
			return (int) tilestr.charAt(0) + (int) tilestr.charAt(1) * 256
					+ (int) tilestr.charAt(2) * 256 * 256
					+ (int) tilestr.charAt(3) * 256 * 256 * 256;
		default:
			System.out.println("Warning: string '" + tilestr
					+ " has wrong size.");
			// XXX no reference to dbg
			// dbgPrint("Warning: string '"+tilestr+" has wrong size.");
			return 0;
		}
	}

	/** tileid==0 -&gt; empty string */
	private String tileidToString(int tileid) {
		if (tileid == 0)
			return "";
		StringBuffer tilestr = new StringBuffer("" + (char) (tileid & 255));
		if (tileid >= 0x100)
			tilestr.append((char) ((tileid / 0x100) & 255));
		if (tileid >= 0x10000)
			tilestr.append((char) ((tileid / 0x10000) & 255));
		if (tileid >= 0x1000000)
			tilestr.append((char) ((tileid / 0x1000000) & 255));
		return tilestr.toString();
	}

	public boolean getTiles(JGRectangle dest, JGRectangle r) {
		if (r == null)
			return false;
		dest.copyFrom(r);
		convertToTiles(dest, r);
		return true;
	}

	void convertToTiles(JGRectangle dest, JGRectangle r) {
		if (dest.x >= 0) {
			dest.x /= tilex;
			dest.width = 1 - dest.x + (r.x + r.width - 1) / tilex;
		} else {
			dest.x = (dest.x - tilex + 1) / tilex;
			dest.width = 1 - dest.x + (r.x + r.width - 1 - tilex + 1) / tilex;
		}
		if (dest.y >= 0) {
			dest.y /= tiley;
			dest.height = 1 - dest.y + (r.y + r.height - 1) / tiley;
		} else {
			dest.y = (dest.y - tiley + 1) / tiley;
			dest.height = 1 - dest.y + (r.y + r.height - 1 - tilex + 1) / tiley;
		}
	}

	public JGRectangle getTiles(JGRectangle r) {
		if (r == null)
			return null;
		JGRectangle tiler = new JGRectangle(r);
		convertToTiles(tiler, r);
		return tiler;
		/*
		 * if (tiler.x >= 0) { tiler.x /= tilex; } else { tiler.x =
		 * (tiler.x-tilex+1)/tilex; } if (tiler.y >= 0) { tiler.y /= tiley; }
		 * else { tiler.y = (tiler.y-tiley+1)/tiley; } tiler.width = 1 - tiler.x
		 * + (r.x + r.width - 1) / tilex; tiler.height = 1 - tiler.y + (r.y +
		 * r.height - 1) / tiley; return tiler;
		 */
	}

	/* background methods from engine */

	public void setTileCid(int x, int y, int value) {
		setTileCid(x, y, 0, value);
	}

	public void orTileCid(int x, int y, int or_mask) {
		setTileCid(x, y, -1, or_mask);
	}

	public void andTileCid(int x, int y, int and_mask) {
		setTileCid(x, y, and_mask, 0);
	}

	public void setTile(JGPoint tileidx, String tilename) {
		setTile(tileidx.x, tileidx.y, tilename);
	}

	public void setTiles(int xofs, int yofs, String[] tilemap) {
		for (int y = 0; y < tilemap.length; y++) {
			for (int x = 0; x < tilemap[y].length(); x++) {
				setTile(x + xofs, y + yofs,
						new String(tilemap[y].substring(x, x + 1)));
			}
		}
	}

	public void setTilesMulti(int xofs, int yofs, String[] tilemap) {
		for (int y = 0; y < tilemap.length; y++) {
			Vector tokens = tokenizeString(tilemap[y], ' ');
			// StringTokenizer toker = new StringTokenizer(tilemap[y]," ");
			int x = 0;
			for (Enumeration e = tokens.elements(); e.hasMoreElements();) {
				setTile(x + xofs, y + yofs, (String) e.nextElement());
				x++;
			}
		}
	}

	public int getTileCidAtCoord(double x, double y) {
		int xidx = (int) x / tilex;
		int yidx = (int) y / tiley;
		return getTileCid(xidx, yidx);
	}

	public int getTileCid(JGPoint center, int xofs, int yofs) {
		return getTileCid(center.x + xofs, center.y + yofs);
	}

	public String getTileStrAtCoord(double x, double y) {
		int xidx = (int) x / tilex;
		int yidx = (int) y / tiley;
		return getTileStr(xidx, yidx);
	}

	public String getTileStr(JGPoint center, int xofs, int yofs) {
		return getTileStr(center.x + xofs, center.y + yofs);
	}

	public void drawImageString(JGEngineInterface eng, String string, double x,
			double y, int align, String imgmap, int char_offset, int spacing,
			boolean pf_relative) {
		ImageMap map = (ImageMap) imagemaps.get(imgmap);
		if (map == null)
			throw new JGameError("Font image map '" + imgmap + "' not found.",
					true);
		if (align == 0) {
			x -= (map.tilex + spacing) * string.length() / 2;
		} else if (align == 1) {
			x -= (map.tilex + spacing) * string.length();
		}
		// Image img = map.getScaledImage();
		StringBuffer lettername_buf = new StringBuffer(imgmap + "# ");
		int lastchar = lettername_buf.length() - 1;
		String lettername = null;
		for (int i = 0; i < string.length(); i++) {
			int imgnr = -char_offset + string.charAt(i);
			// String lettername = imgmap+"#"+string.charAt(i);
			lettername_buf.setCharAt(lastchar, string.charAt(i));
			lettername = lettername_buf.toString();
			if (!existsImage(lettername)) {
				defineImage(lettername, "FONT", 0, getSubImage(imgmap, imgnr),
						"-", 0, 0, 0, 0);
			}
			JGImage letter = getImage(lettername);
			eng.drawImage(x, y, lettername, pf_relative);
			// eng.drawImage(buf_gfx, x,y,lettername,pf_relative);
			x += map.tilex + spacing;
		}
	}

	/* ====== math ====== */

	// XXX replace moduloX/YPos by versions without complexity!

	public double moduloXPos(double x) {
		while (x - xofs_mid > pfwidth_half) {
			x -= pfwidth_half * 2;
		}
		while (x - xofs_mid < -pfwidth_half) {
			x += pfwidth_half * 2;
		}
		return x;
	}

	public double moduloYPos(double y) {
		while (y - yofs_mid > pfheight_half) {
			y -= pfheight_half * 2;
		}
		while (y - yofs_mid < -pfheight_half) {
			y += pfheight_half * 2;
		}
		return y;
	}

	/**
	 * Div which always rounds downwards, also for negative numbers. It is the
	 * counterpart of moduloFloor.
	 */
	public int divFloor(int pos, int div) {
		return (int) Math.floor((double) pos / (double) div);
	}

	/**
	 * Modulo according to a regular modulo space, resulting in a value that is
	 * always greater than 0, float version. It is the counterpart of divFloor.
	 */
	public double moduloFloor(double pos, int div) {
		return pos - div * Math.floor(pos / div);
	}

	/**
	 * Modulo according to a regular modulo space, resulting in a value that is
	 * always greater than 0.
	 */
	public int moduloFloor(int pos, int modulo) {
		// int result;
		if (pos >= 0)
			return pos % modulo;
		return modulo - 1 - ((-1 - pos) % modulo);
		// return (modulo - ((-pos)%modulo))%modulo;
		// System.out.println(pos+" modulo "+modulo+" = "+result);
		// return result;
	}

	/**
	 * The scale methods also take care of wraparound modulo calculations. This
	 * is only applicable to pf_relative positions. The modulo semantics is that
	 * of moduloPos.
	 */

	public int scaleXPos(double x, boolean pf_relative) {
		if (!pf_relative) {
			return (int) Math.floor(x_scale_fac * x);
		} else {
			if (pf_wrapx)
				x = moduloXPos(x);
			return (int) Math.floor(x_scale_fac * x) - xofs_scaled;
		}
	}

	public int scaleYPos(double y, boolean pf_relative) {
		if (!pf_relative) {
			return (int) Math.floor(y_scale_fac * y);
		} else {
			if (pf_wrapy)
				y = moduloYPos(y);
			return (int) Math.floor(y_scale_fac * y) - yofs_scaled;
		}
	}

	public JGPoint scalePos(double x, double y, boolean pf_relative) {
		// XXX inline scaleXPos/scaleYPos
		return new JGPoint(scaleXPos(x, pf_relative), scaleYPos(y, pf_relative));
	}

	/** returns null if r is null */
	public JGRectangle scalePos(double x, double y, double width,
			double height, boolean pf_relative) {
		// XXX that's a lot of object creation
		JGPoint topleft = scalePos(x, y, pf_relative);
		JGPoint botright = scalePos(x + width, y + height, pf_relative);
		// XXX a rectangle on the wrap border is a singular case
		// XXX rounding is not pixel-perfect for this case
		if (botright.x < topleft.x)
			botright.x = topleft.x + scaleXPos(width, pf_relative);
		if (botright.y < topleft.y)
			botright.y = topleft.y + scaleYPos(height, pf_relative);
		return new JGRectangle(topleft.x, topleft.y,
				botright.x - topleft.x - 1, botright.y - topleft.y - 1);
	}

	/** returns null if r is null */
	public JGRectangle scalePos(JGRectangle r, boolean pf_relative) {
		if (r == null)
			return null;
		return scalePos(r.x, r.y, r.width, r.height, pf_relative);
	}

	/* misc */

	/**
	 * Initialise some derived pf dimension variables and clear the tile map.
	 */
	public void initPF() {
		int allowed_width = winwidth + crop_left + crop_right;
		int allowed_height = winheight + crop_top + crop_bottom;
		// these came from the canvas constructor
		viewnrtilesx = nrtilesx;
		viewnrtilesy = nrtilesy;
		if (prescale) {
			scaledtilex = allowed_width / nrtilesx;
			scaledtiley = allowed_height / nrtilesy;
			double aspectratio = (scaledtilex / (double) scaledtiley)
					/ (tilex / (double) tiley);
			if (aspectratio < min_aspect) {
				// y is too large
				scaledtiley = (int) (scaledtilex / min_aspect);
			} else if (aspectratio > max_aspect) {
				// x is too large
				scaledtilex = (int) (max_aspect * scaledtiley);
			}
			width = scaledtilex * nrtilesx;
			height = scaledtiley * nrtilesy;
		} else {
			scaledtilex = tilex;
			scaledtiley = tiley;
			// XXX aspect ratio is not constrained yet in this case!
			width = winwidth;
			height = winheight;
		}
		x_scale_fac = width / (double) (tilex * nrtilesx);
		y_scale_fac = height / (double) (tiley * nrtilesy);
		min_scale_fac = Math.min(x_scale_fac, y_scale_fac);
		initBGTiles(nrtilesx, nrtilesy, "");
		// now, calculate the offsets.
		if (prescale) {
			int x_excess = width - winwidth;
			int y_excess = height - winheight;
			// balance is a number between -1 (shift left) and 1 (shift right)
			double xbalance = 0, ybalance = 0;
			if (crop_left + crop_right > 0 && x_excess > 0)
				xbalance = (crop_right - crop_left) / (crop_left + crop_right);
			if (crop_top + crop_bottom > 0 && y_excess > 0)
				ybalance = (crop_bottom - crop_top) / (crop_top + crop_bottom);
			canvas_xofs = (int) (-x_excess * (0.5 - 0.5 * xbalance));
			canvas_yofs = (int) (-y_excess * (0.5 - 0.5 * ybalance));
		} // else use default offsets of 0.0
	}

	public void setViewOffset(int xofs, int yofs, boolean centered) {
		if (centered) {
			xofs -= viewWidth() / 2;
			yofs -= viewHeight() / 2;
		}
		setPendingViewOffset(xofs, yofs);
		JGObject.updateEngineSettings();
	}

	public void setView(int xofs, int yofs, boolean centered) {
		if (centered) {
			xofs -= viewWidth() / 2;
			yofs -= viewHeight() / 2;
		}
		setPendingView(xofs, yofs);
		JGObject.updateEngineSettings();
	}

	public void setBGImgOffset(int depth, double xofs, double yofs,
			boolean centered) {
		if (centered) {
			xofs -= viewWidth() / 2;
			yofs -= viewHeight() / 2;
		}
		if (bg_images.size() < depth)
			throw new JGameError("Parallax depth " + depth + " not defined.");
		BGImage bgimg = (BGImage) bg_images.elementAt(depth);
		bgimg.xofs = xofs;
		bgimg.yofs = yofs;
	}

	public void setPFSize(int nrtilesx, int nrtilesy) {
		initBGTiles(nrtilesx, nrtilesy, "");
		JGObject.updateEngineSettings();
	}

	public void setPFWrap(boolean wrapx, boolean wrapy, int shiftx, int shifty) {
		pf_wrapx = wrapx;
		pf_wrapy = wrapy;
		pf_wrapshiftx = shiftx;
		pf_wrapshifty = shifty;
		// ensure offset is properly clipped
		setViewOffset(xofs, yofs, false);
		calcPFWrapCenter();
		JGObject.updateEngineSettings();
	}

	public void setFrameRate(double fps, double maxframeskip) {
		this.fps = fps;
		this.maxframeskip = maxframeskip;
	}

	public void setRenderSettings(int alpha_thresh, JGColor render_bg_col) {
		this.alpha_thresh = alpha_thresh;
		this.render_bg_color = render_bg_col;
	}

	public void setOffscreenMargin(int xmargin, int ymargin) {
		offscreen_margin_x = xmargin;
		offscreen_margin_y = ymargin;
	}

	public void setGameSpeed(double speed) {
		gamespeed = speed + 0.00000000001;
		JGObject.updateEngineSettings();
	}

	/* timers */

	public void removeAllTimers() {
		timers.removeAllElements();
	}

	public void registerTimer(JGTimer timer) {
		timers.addElement(timer);
	}

	/** protected */
	public void tickTimers() {
		for (int i = timers.size() - 1; i >= 0; i--) {
			JGTimer timer = (JGTimer) timers.elementAt(i);
			if (timer.tick(gamespeed)) {
				timers.removeElement(timer);
			}
		}
	}

	/* game state */

	public void setGameState(String state) {
		boolean already_in_state = inGameStateNextFrame(state);
		gamestate_nextframe.removeAllElements();
		gamestate_nextframe.addElement(state);
		gamestate_new.removeAllElements();
		if (!already_in_state)
			gamestate_new.addElement(state);
	}

	public void addGameState(String state) {
		if (!inGameStateNextFrame(state)) {
			gamestate_nextframe.addElement(state);
			gamestate_new.addElement(state);
		}
	}

	public void removeGameState(String state) {
		gamestate_nextframe.removeElement(state);
		gamestate_new.removeElement(state);
	}

	public void clearGameState() {
		gamestate_nextframe.removeAllElements();
	}

	public boolean inGameState(String state) {
		for (int i = gamestate.size() - 1; i >= 0; i--) {
			if (((String) gamestate.elementAt(i)).equals(state))
				return true;
		}
		return false;
	}

	public boolean inGameStateNextFrame(String state) {
		for (int i = gamestate_nextframe.size() - 1; i >= 0; i--) {
			if (((String) gamestate_nextframe.elementAt(i)).equals(state))
				return true;
		}
		return false;
	}

	/* ====== animation ====== */

	public void defineAnimation(String id, String[] frames, double speed) {
		animations.put(id, new Animation(frames, speed));
	}

	public void defineAnimation(String id, String[] frames, double speed,
			boolean pingpong) {
		animations.put(id, new Animation(frames, speed, pingpong));
	}

	public Animation getAnimation(String id) {
		return (Animation) animations.get(id);
	}

	/* computation */

	public boolean and(int value, int mask) {
		return (value & mask) != 0;
	}

	public double random(double min, double max) {
		return min + random.nextDouble() * (max - min);
	}

	public double random(double min, double max, double interval) {
		int steps = (int) Math.floor(0.00001 + (max - min) / interval);
		return min + ((int) (random.nextDouble() * (steps + 0.99))) * interval;
	}

	public int random(int min, int max, int interval) {
		int steps = (max - min) / interval;
		return min + ((int) (random.nextDouble() * (steps + 0.99))) * interval;
	}

	public JGPoint getTileIndex(double x, double y) {
		return new JGPoint((int) Math.floor(x / (double) tilex),
				(int) Math.floor(y / (double) tiley));
	}

	public JGPoint getTileCoord(int tilex, int tiley) {
		return new JGPoint(tilex * tileWidth(), tiley * tileHeight());
	}

	public JGPoint getTileCoord(JGPoint tileidx) {
		return new JGPoint(tileidx.x * tileWidth(), tileidx.y * tileHeight());
	}

	public double snapToGridX(double x, double gridsnapx) {
		if (gridsnapx <= 0.0)
			return x;
		int xaligned = tilex
				* (int) Math.floor(((x + tilex / 2.0) / (double) tilex));
		double gridofsx = Math.abs(x - xaligned);
		if (gridofsx <= gridsnapx + 0.0002)
			return xaligned;
		return x;
	}

	public double snapToGridY(double y, double gridsnapy) {
		if (gridsnapy <= 0.0)
			return y;
		int yaligned = tiley
				* (int) Math.floor(((y + tiley / 2.0) / (double) tiley));
		double gridofsy = Math.abs(y - yaligned);
		if (gridofsy <= gridsnapy + 0.0002)
			return yaligned;
		return y;
	}

	public void snapToGrid(JGPoint p, int gridsnapx, int gridsnapy) {
		if (gridsnapx == 0 && gridsnapy == 0)
			return;
		int xaligned = tilex
				* (int) Math.floor(((p.x + tilex / 2.0) / (double) tilex));
		int yaligned = tiley
				* (int) Math.floor(((p.y + tiley / 2.0) / (double) tiley));
		int gridofsx = Math.abs(p.x - xaligned);
		int gridofsy = Math.abs(p.y - yaligned);
		if (gridofsx <= gridsnapx)
			p.x = xaligned;
		if (gridofsy <= gridsnapy)
			p.y = yaligned;
	}

	public boolean isXAligned(double x, double margin) {
		if (margin < 0)
			margin = 0.0;
		int xaligned = tilex * (int) (((int) x + tilex / 2) / tilex);
		return Math.abs(x - xaligned) <= margin + 0.00005;
	}

	public boolean isYAligned(double y, double margin) {
		if (margin < 0)
			margin = 0.0;
		int yaligned = tiley * (int) (((int) y + tiley / 2) / tiley);
		return Math.abs(y - yaligned) <= margin + 0.00005;
	}

	public double getXAlignOfs(double x) {
		int xaligned = tilex * (int) (((int) x + tilex / 2) / tilex);
		return x - xaligned;
	}

	public double getYAlignOfs(double y) {
		int yaligned = tiley * (int) (((int) y + tiley / 2) / tiley);
		return y - yaligned;
	}

	// XXX please test these two methods

	public double getXDist(double x1, double x2) {
		if (pf_wrapx) {
			double x1mod = moduloFloor(x1, pfwidth);
			double x2mod = moduloFloor(x2, pfwidth);
			return Math.min(Math.abs(x1mod - x2mod),
					Math.abs(x1mod + pfwidth - x2mod));
		} else {
			return Math.abs(x1 - x2);
		}
	}

	public double getYDist(double y1, double y2) {
		if (pf_wrapy) {
			int pfheight = pfheight_half * 2;
			double y1mod = moduloFloor(y1, pfheight);
			double y2mod = moduloFloor(y2, pfheight);
			return Math.min(Math.abs(y1mod - y2mod),
					Math.abs(y1mod + pfheight - y2mod));
		} else {
			return Math.abs(y1 - y2);
		}
	}

	/* === audio === */

	/** clipid -} filename */
	public Hashtable audioclips = new Hashtable();

	/**
	 * Associate given clipid with a filename. Files are loaded from the
	 * resource path. Java 1.2+ supports at least: midi and wav files.
	 */

	public void defineAudioClip(Object pkg_obj, String clipid, String filename) {
//		filename = getAbsolutePath(pkg_obj, filename);
		audioclips.put(clipid, filename);
		// XXX we should replace the old clip.
		// replace requires all old audioclip instances to be deleted.
	}

	public int screenCoordinateX() {
		return xofs;
	}

	public int screenCoordinateY() {
		return yofs;
	}

}
